/* fat_dir.c
 *
 * Copyright (C) 2010 Ricardo R. Massaro
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "fat_dir.h"
#include "pack.h"

#define ATTR_READ_ONLY  0x01
#define ATTR_HIDDEN     0x02
#define ATTR_SYSTEM     0x04
#define ATTR_VOLUME_ID  0x08
#define ATTR_DIRECTORY  0x10
#define ATTR_ARCHIVE    0x20
#define ATTR_LONG_NAME  (ATTR_READ_ONLY|ATTR_HIDDEN|ATTR_SYSTEM|ATTR_VOLUME_ID)

#define MAKE_DATE(y,m,d)   ( (((int)(d))&0x1f) | ((((int)(m))&0xf)<<5) | (((((int)(y))-1980)&0x7f)<<9) )
#define MAKE_TIME(h,m,s)   ( ((((int)(s))/2)&0x1f) | ((((int)(m))&0x3f)<<5) | ((((int)(h))&0x1f)<<11) )

void fat_dir_init(FAT_DIR *dir, unsigned char *cluster_data, int first_cluster, int n_clusters, int parent_cluster)
{
  dir->cluster_data = cluster_data;
  dir->first_cluster = first_cluster;
  dir->n_clusters = n_clusters;
  dir->parent_cluster = parent_cluster;

  dir->max_entries = n_clusters * 64 * 512 / 32;
  dir->used_entries = 0;
}

static unsigned char calc_filename_checksum(const unsigned char *filename)
{
  unsigned char checksum;
  int i;

  checksum = 0;
  for (i = 0; i < 11; i++)
    checksum = ((checksum & 1) ? 0x80 : 0) + (checksum >> 1) + filename[i];
  return checksum;
}

static int num_digits(int n)
{
  int ret = 0;

  while (n) {
    n /= 10;
    ret++;
  }
  return ret;
}

static int mangle_filename(FAT_DIR *dir, unsigned char *mangled, const char *filename)
{
  int n, force_tail, name_len;
  const char *p, *ext;

  force_tail = 0;

  ext = strrchr(filename, '.');
  if (! ext)
    ext = filename + strlen(filename);

  // copy name
  p = filename;
  if (*p == '.') {
    force_tail = 1;
    while (*p == '.')
      p++;
  }
  n = 0;
  while (n < 8 && p < ext) {
    if (*p > ' ' && strchr(".\"*+,:;<=>?[\\]\x7f", *p) == NULL)
      mangled[n++] = toupper(*p);
    else
      force_tail = 1;
    p++;
  }
  name_len = n;
  if (p < ext)
    force_tail = 1;
  while (n < 8)
    mangled[n++] = ' ';

  // copy ext
  p = ext;
  if (*p) p++; // skip '.'
  n = 0;
  if (p != NULL) {
    while (n < 3 && *p != '\0') {
      if (*p > ' ' && strchr("\"*+,:;<=>?[\\]\x7f", *p) == NULL)
	mangled[8 + n++] = toupper(*p);
      else
	force_tail = 1;
      p++;
    }
    if (*p)
      force_tail = 1;
  }
  while (n < 3)
    mangled[8 + n++] = ' ';
  
  // add tail as necessary
  for (n = (force_tail) ? 1 : 0; n <= 999999; n++) {
    int i, has_collision;

    if (n > 0) {  // add or replace tail "~n"
      int k, tail_end;
      unsigned char *p;

      tail_end = name_len + num_digits(n);
      if (tail_end > 7)
	tail_end = 7;
      p = mangled + tail_end;
      k = n;
      while (k > 0) {
	*p-- = '0' + k % 10;
	k /= 10;
      }
      *p = '~';
    }

    //printf("trying '%.11s'\n", mangled);

    has_collision = 0;
    for (i = 0; i < dir->used_entries; i++)
      if (memcmp(dir->cluster_data + 32*i, mangled, 11) == 0) {
	has_collision = 1;
	break;
      }
    if (! has_collision)
      break;
  }
  if (n > 999999)
    return -1;
  return calc_filename_checksum(mangled);
}

static unsigned char conv_lfn_char(char ch)
{
  if (ch != 0 && strchr("\"*:<>?\\\x7f", ch))
    return '_';
  return ch;
}

static void copy_lfn_frag(unsigned char *dest, const char *src, int src_off, int src_len, int copy_len)
{
  int i;

  for (i = 0; i < copy_len; i++)
    if (i + src_off < src_len) {
      *dest++ = conv_lfn_char(src[i + src_off]);
      *dest++ = 0;
    } else if (i + src_off == src_len) {
      *dest++ = 0;
      *dest++ = 0;
    } else {
      *dest++ = 0xff;
      *dest++ = 0xff;
    }
}

static int add_lfn_entries(FAT_DIR *dir, const char *filename, unsigned char checksum)
{
  int n_entries, len, i;

  len = strlen(filename);
  n_entries = (len + 12) / 13;
  if (dir->used_entries + n_entries >= dir->max_entries)
    return -1;

  for (i = n_entries-1; i >= 0; i--) {
    // add entry with name 'filename+13*i'
    unsigned char *p = dir->cluster_data + 32*dir->used_entries++;

    pack_u8_le(p, (i+1) | ((i == n_entries-1) ? 0x40 : 0));
    copy_lfn_frag(p + 1, filename + 13*i, 0, len - 13*i, 5);
    pack_u8_le(p + 11, ATTR_LONG_NAME);
    pack_u8_le(p + 12, 0);
    pack_u8_le(p + 13, checksum);
    copy_lfn_frag(p + 14, filename + 13*i, 5, len - 13*i, 6);
    pack_u16_le(p + 26, 0);
    copy_lfn_frag(p + 28, filename + 13*i, 11, len - 13*i, 2);
  }

  return n_entries;
}

/**
 * Add an entry to the directory, return the number of the entry or -1
 * on error (no space available).
 *
 * Tha calling function must fill all entry information other than
 * name (size, cluster, attributes, dates, etc.).
 *
 * More than one entry may be created if the filename is long.  In
 * that case, the returned entry will be the short name entry that
 * must have the information filled.
 */
static int add_entry(FAT_DIR *dir, const char *filename, int short_only)
{
  unsigned char mangled[11];

  if (dir->used_entries >= dir->max_entries)
    return -1;
  
  if (short_only) {
    int i;

    memset(mangled, ' ', 11);
    if (filename[0] == '.')
      memcpy(mangled, filename, strlen(filename));
    else {
      for (i = 0; i < 8 && filename[i] != '.' && filename[i] != '\0'; i++)
	mangled[i] = toupper(filename[i]);
      filename = strchr(filename, '.');
      if (filename) {
	filename++;
	for (i = 0; i < 3 && filename[i] != '\0'; i++)
	  mangled[i+8] = toupper(filename[i]);    
      }
    }
  } else {
    int checksum;
    int n_lfn_entries;

    // calculate mangled name
    checksum = mangle_filename(dir, mangled, filename);
    if (checksum < 0)
      return -1;
    
    // add long filename entries
    n_lfn_entries = add_lfn_entries(dir, filename, (unsigned char) checksum);
    if (n_lfn_entries < 0)
      return -1;
  }

  // copy mangled name
  memcpy(dir->cluster_data + 32*dir->used_entries, mangled, 11);
  return dir->used_entries++;
}

static unsigned int timestamp_fat_date_time(time_t t)
{
  struct tm tm;
  unsigned int fat_time, fat_date;

  localtime_r(&t, &tm);
  fat_time = MAKE_TIME(tm.tm_hour, tm.tm_min, tm.tm_sec);
  fat_date = MAKE_DATE(tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday);
  return (fat_date<<16) | fat_time;
}

int fat_dir_add_volume_label(FAT_DIR *dir, const char *label)
{
  int entry;
  time_t mtime;
  unsigned int fat_mtime;
  unsigned char *p;
  
  entry = add_entry(dir, label, 1);
  if (entry < 0)
    return -1;

  mtime = time(NULL);
  fat_mtime = timestamp_fat_date_time(mtime);

  p = dir->cluster_data + 32 * entry;
  pack_u8_le(p + 11, ATTR_VOLUME_ID);       // type
  pack_u8_le(p + 12, 0);                    // reserved
  pack_u8_le(p + 13, 100*(mtime&1));        // second/100 of creation time
  pack_u16_le(p + 14, fat_mtime&0xffff);    // creation time
  pack_u16_le(p + 16, fat_mtime>>16);       // creation date
  pack_u16_le(p + 18, fat_mtime>>16);       // last access date
  pack_u16_le(p + 20, 0);                   // 1st cluster high
  pack_u16_le(p + 22, fat_mtime&0xffff);    // last write time
  pack_u16_le(p + 24, fat_mtime>>16);       // last write date
  pack_u16_le(p + 26, 0);                   // 1st cluster low
  pack_u32_le(p + 28, 0);                   // size
  return 0;
}

/**
 * Add a file to the directory, return the number of clusters or -1 on error.
 */
int fat_dir_add_file(FAT_DIR *dir, const char *filepath, int start_cluster, int num_free_clusters)
{
  struct stat st;
  const char *filename;
  unsigned char *p;
  unsigned int fat_atime, fat_mtime;
  unsigned int entry;
  unsigned int file_size;
  int n_clusters;
  
  // get file information
  if (stat(filepath, &st) != 0)
    return -1;
  fat_atime = timestamp_fat_date_time(st.st_atime);
  fat_mtime = timestamp_fat_date_time(st.st_mtime);
  file_size = st.st_size;
  if (file_size == 0)
    start_cluster = 0;

  // check if there's enough space
  n_clusters = (file_size + (64*512-1)) / (64*512);
  if (n_clusters > num_free_clusters)
    return -2;

  // get file name
  filename = strrchr(filepath, '/');
  if (filename)
    filename++;
  else
    filename = filepath;

  // create dir entry
  entry = add_entry(dir, filename, 0);
  if (entry < 0)
    return -3;

  p = dir->cluster_data + 32 * entry;
  pack_u8_le(p + 11, 0);                     // type
  pack_u8_le(p + 12, 0);                     // reserved
  pack_u8_le(p + 13, 100*(st.st_mtime&1));   // second/100 of creation time
  pack_u16_le(p + 14, fat_mtime&0xffff);     // creation time
  pack_u16_le(p + 16, fat_mtime>>16);        // creation date
  pack_u16_le(p + 18, fat_atime>>16);        // last access date
  pack_u16_le(p + 20, start_cluster>>16);    // 1st cluster high
  pack_u16_le(p + 22, fat_mtime&0xffff);     // last write time
  pack_u16_le(p + 24, fat_mtime>>16);        // last write date
  pack_u16_le(p + 26, start_cluster&0xffff); // 1st cluster low
  pack_u32_le(p + 28, file_size);            // size
  return n_clusters;
}

/**
 * Add a file to the directory, return the number of clusters or -1 on error.
 */
int fat_dir_add_dir(FAT_DIR *dir, const char *filepath, int start_cluster)
{
  struct stat st;
  const char *filename;
  unsigned char *p;
  unsigned int fat_atime, fat_mtime;
  unsigned int entry;
  int force_short;

  // get file information
  if (stat(filepath, &st) != 0)
    return -1;
  fat_atime = timestamp_fat_date_time(st.st_atime);
  fat_mtime = timestamp_fat_date_time(st.st_mtime);

  // get file name
  filename = strrchr(filepath, '/');
  if (filename)
    filename++;
  else
    filename = filepath;

  if (strcmp(filename, ".") == 0 || strcmp(filename, "..") == 0)
    force_short = 1;
  else
    force_short = 0;  

  // create dir entry
  entry = add_entry(dir, filename, force_short);
  if (entry < 0)
    return -1;

  p = dir->cluster_data + 32 * entry;
  pack_u8_le(p + 11, ATTR_DIRECTORY);        // type
  pack_u8_le(p + 12, 0);                     // reserved
  pack_u8_le(p + 13, 100*(st.st_mtime&1));   // second/100 of creation time
  pack_u16_le(p + 14, fat_mtime&0xffff);     // creation time
  pack_u16_le(p + 16, fat_mtime>>16);        // creation date
  pack_u16_le(p + 18, fat_atime>>16);        // last access date
  pack_u16_le(p + 20, start_cluster>>16);    // 1st cluster high
  pack_u16_le(p + 22, fat_mtime&0xffff);     // last write time
  pack_u16_le(p + 24, fat_mtime>>16);        // last write date
  pack_u16_le(p + 26, start_cluster&0xffff); // 1st cluster low
  pack_u32_le(p + 28, 0);                    // size
  return 0;
}

